Disabling Motion in Next
In the previous lesson, I mentioned that it's a good idea to wrap the <MotionConfig>
element around the entirety of our React application, to ensure that all motion
elements respect the “Reduce motion” preference:
import React from 'react';import { MotionConfig } from 'framer-motion';
function App() { return ( <MotionConfig reducedMotion="user"> {/* The entire application here */} </MotionConfig> );}
export default App;
This works for a client-side build tool like Parcel, but how does it work in the context of Next.js? We don't have an App.js
in Next.
The closest thing we have to an App
component is the root layout, in /src/app/layout.js
. Here's my initial attempt at a solution:
// /src/app/layout.jsimport React from 'react';import { MotionConfig } from 'framer-motion';
function RootLayout({ children }) { return ( <MotionConfig reducedMotion="user"> <html lang="en"> <body> {children} </body> </html> </MotionConfig> );}
export default RootLayout;
If we do this, though, we'll get an error:
You're importing a component that needs useEffect. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
We're not using useEffect
anywhere, but MotionConfig
does. And, unfortunately, the maintainers of Framer Motion haven't added "use client"
directives.
This is quite common; there are lots of third-party libraries that have not yet been updated to work with React Server Components.
So, how should we fix this? Spend a few moments and consider the problem. We'll dig into the possible solutions below.
The easiest thing would be to add the "use client"
directive to our layout.js
file:
// /src/app/layout.js"use client";import React from 'react';import { MotionConfig } from 'framer-motion';
function RootLayout({ children }) { return ( <MotionConfig reducedMotion="user"> <html lang="en"> <body> {children} </body> </html> </MotionConfig> );}
export default RootLayout;
This works because marking a component as a Client Component affects all components imported by that file. It has the effect of treating MotionConfig
as a Client Component, regardless of whether it includes the "use client"
directive or not.
But hmm, this feels a bit like overkill… I don't need the entire layout.js
file to render on the client!
In this particular situation, our layout.js
component is quite sparse, and so it's not really a problem, but in a real application, it would likely include an entire layout!
Instead, what if we create a client wrapper component? For example:
// src/components/RespectMotionPreferences.js'use client';import React from 'react';import { MotionConfig } from 'framer-motion';
function RespectMotionPreferences({ children }) { return ( <MotionConfig reducedMotion="user"> {children} </MotionConfig> );}
export default RespectMotionPreferences;
I've created a small component with 1 job: to “wrap around” the MotionConfig
component, locking in the reducedMotion
prop and setting the "use client"
directive.
We then use this component instead of MotionConfig
:
// /src/app/layout.jsimport React from 'react';
import RespectMotionPreferences from '@/components/RespectMotionPreferences';
function RootLayout({ children }) { return ( <RespectMotionPreferences> <html lang="en"> <body> {children} </body> </html> </RespectMotionPreferences> );}
export default RootLayout;
With this approach, layout.js
remains a Server Component. We import RespectMotionPreferences
, a Client Component, and pass through all of the UI through the children
prop.
Essentially, we're doing the same thing we did in the “Server Components and styled-components” exercise. We create a new Client Component that wraps around the chunk of code that requires client-side React. That way, we reduce the amount of stuff that has to run on the client.
As I mentioned in Module 6, it takes a while to get used to the “React Server Components” paradigm, so please don't feel bad if it doesn't make much sense to you yet! Intuition will come with practice. ❤️